/******************************************************************************
 * Shared utilities for page and worker scripts.
 *****************************************************************************/

(function (root, factory) {
  // Browser globals
  root.utils = factory();
}(this, function () {

'use strict';

const ASCII_WHITESPACE = /[ \t\n\r\f]/;
const KEYWORD_COLORS = 8;

/** @global */
const utils = {
  ASCII_WHITESPACE,
  KEYWORD_COLORS,
  PAGE_INFO: {},

  crc32(...args) {
    const crcTable = (() => {
      let c;
      let crcTable = [];
      for (let n = 0; n < 256; n++) {
        c = n;
        for (let k = 0; k < 8; k++) {
          c = ((c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1));
        }
        crcTable[n] = c;
      }
      return crcTable;
    })();
    const fn = utils.crc32 = function crc32(str, salt = '') {
      str = str + salt;
      let crc = -1;
      for(let i = 0, I = str.length; i < I; i++) {
        crc = (crc >>> 8) ^ crcTable[(crc ^ str.charCodeAt(i)) & 0xFF];
      }
      const rv = (crc ^ (-1)) >>> 0;
      return rv.toString(16);
    };
    return fn(...args);
  },

  toBaseN(...args) {
    const digits = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
    const fn = utils.toBaseN = (num, radix = 10) => {
      if (radix < 2 || radix >= 63) {
        throw new RangeError('radix must be between 2 and 62');
      }

      if (num === 0) {
        return digits[0];
      }

      // quick floor
      radix = ~~radix;

      let rv = ''; 
      while (num > 0) {
        rv = digits[num % radix] + rv;
        num = ~~(num / radix);
      }

      return rv;
    };
    return fn(...args);
  },

  fromBaseN(...args) {
    const digits = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
    const table = Array.prototype.reduce.call(digits, (rv, x, i) => (rv[x] = i, rv), {});
    const fn = utils.fromBaseN = (str, radix = 10) => {
      if (radix < 2 || radix >= 63) {
        return NaN;
      }

      // quick floor
      radix = ~~radix;

      let rv = NaN, len = str.length;
      for (let i = 0; i < len; i++) {
        const p = table[str[i]];
        if (!(p < radix)) {
          return rv;
        }
        rv = (rv || 0) * radix + p;
      }

      return rv;
    };
    return fn(...args);
  },

  setIsEqual(setA, setB) {
    if (setA.size !== setB.size) { return false; }
    for (const i of setA) {
      if (!setB.has(i)) { return false; }
    }
    return true;
  },

  escapeHtml(...args) {
    const regex = /[&<>"']| (?= )/g;
    const func = m => map[m];
    const map = {
      "&": "&amp;",
      "<": "&lt;",
      ">": "&gt;"
    };
    const fn = utils.escapeHtml = (str, {
      noDoubleQuotes = false,
      singleQuotes = false,
      spaces = false,
    } = {}) => {
      map['"'] = noDoubleQuotes ? '"' : "&quot;";
      map["'"] = singleQuotes ? "&#39;" : "'";
      map[" "] = spaces ? "&nbsp;" : " ";
      return str.replace(regex, func);
    };
    return fn(...args);
  },

  escapeRegExp(str) {
    // 不脫義 "-"，因 input[pattern] 不允許脫義 "-"
    // Escaping "-" allows the result to be inserted into a character class.
    // Escaping "/" allow the result to be used in a JS regex literal.
    const regex = /[/\\^$*+?.|()[\]{}]/g;
    const fn = utils.escapeRegExp = (str) => {
      return str.replace(regex, "\\$&");
    };
    return fn(str);
  },

  /**
   * @param {WeakMap|Map} map - 用快取加速取得大量連續的 Xpath，但 refNode 和 DOM 皆不可變動
   */
  getXPath(node, refNode, map) {
    const regex = /\[(\d+)\]$/;

    const getXPathInternal = (node, refNode, map) => {
      if (!node || node === refNode) {
        return '';
      }

      let xpath;
      let name = node.nodeName;
      let i = 0;
      let sibling = node;
      while (sibling) {
        if (sibling.nodeName === name) {
          if (map && (xpath = map.get(sibling))) {
            if (sibling === node) {
              return xpath;
            }

            xpath = xpath.replace(regex, (_, idx) => `[${parseInt(idx, 10) + i}]`);
            map.set(node, xpath);
            return xpath;
          }
          i++;
        }
        sibling = sibling.previousSibling;
      }

      xpath = getXPathInternal(node.parentNode, refNode);
      switch (node.nodeType) {
        case 1:
          xpath += `/${name}[${i}]`;
          break;
        case 3:
          xpath += `/text()[${i}]`;
          break;
        case 8:
          xpath += `/comment()[${i}]`;
          break;
        default:
          throw new Error(`Unsupported node type: ${node.nodeName}".`);
      }
      if (xpath[0] === '/') { xpath = xpath.slice(1); }

      map && map.set(node, xpath);
      return xpath;
    };

    const fn = utils.getXPath = (node, refNode, map) => {
      if (!node) {
        return null;
      }

      if (node === refNode) {
        return '.';
      }

      return getXPathInternal(node, refNode, map);
    };

    return fn(node, refNode, map);
  },

  /**
   * 按 WHATWG 規範解析 dl 元素的鍵值組
   *
   * ref: https://html.spec.whatwg.org/multipage/grouping-content.html#the-dl-element
   */
  parseDl(elem) {
    const processDtDd = (elem) => {
      switch (elem.nodeName.toLowerCase()) {
        case 'dt':
          if (seenDd) {
            groups.push(current);
            current = {name: [], value: []};
            seenDd = false;
          }
          current.name.push(elem);
          break;
        case 'dd':
          current.value.push(elem);
          seenDd = true;
          break;
      }
    };

    const groups = [];
    let current = {name: [], value: []};
    let seenDd = false;
    let child = elem.firstChild;
    let grandchild = null;

    while (child) {
      switch (child.nodeName.toLowerCase()) {
        case 'div':
          grandchild = child.firstChild;
          while (grandchild) {
            processDtDd(grandchild);
            grandchild = grandchild.nextSibling;
          }
          break;
        default:
          processDtDd(child);
          break;
      }
      child = child.nextSibling;
    }

    if (current.name.length || current.value.length) {
      groups.push(current);
    }

    return groups;
  },

  /**
   * 將 dl 元素的鍵值組解析為 JSON 物件
   *
   * - dt, dd 可多對多組合（dl.元資料只支援一對一）
   * - dt, dd 中若有 data[value] 則取其值（多個 data 相當於多個 dt/dd），
   *   否則取 textContent
   *
   * @param {boolean} readRaw - 忽略 data[value] 只讀取 textContent 值
   * @return {Object.<string, string[]>}
   */
  loadDlKeyValue(elem, readRaw = false) {
    const getElemValues = (stack, elem) => {
      const dataElems = elem.querySelectorAll('data[value]');
      if (dataElems.length && !readRaw) {
        for (const dataElem of dataElems) {
          // Google Chrome < 62 does not support HTMLDataElement.value
          stack.push(dataElem.getAttribute('value'));
        }
      } else {
        stack.push(elem.textContent.trim());
      }
      return stack;
    };

    const groups = utils.parseDl(elem);
    const metadata = {};
    for (const group of groups) {
      const names = group.name.reduce(getElemValues, []);
      const values = group.value.reduce(getElemValues, []);
      for (const name of names) {
        if (!metadata[name]) {
          metadata[name] = [];
        }
        for (const value of values) {
          metadata[name].push(value);
        }
      }
    }
    return metadata;
  },

  tidyCssPropertyValue(...args) {
    const dummyElem = document.createElement('span');

    const tidyCssPropertyValue = this.tidyCssPropertyValue = (propertyName, value) => {
      // the style property won't change if value is invalid,
      // and the got value will generally become "".
      dummyElem.style.removeProperty(propertyName);
      dummyElem.style.setProperty(propertyName, value);
      return dummyElem.style.getPropertyValue(propertyName);
    };

    return tidyCssPropertyValue(...args);
  },

  xhr(params = {}) {
    return new Promise((resolve, reject) => {
      const xhr = new XMLHttpRequest();

      if (params.onreadystatechange) {
        xhr.onreadystatechange = function (event) {
          params.onreadystatechange(xhr);
        };
      }

      xhr.onload = function (event) {
        if (xhr.status == 200 || xhr.status == 0) {
          // we only care about real loading success
          resolve(xhr);
        } else {
          // treat "404 Not found" or so as error
          let statusText = xhr.statusText || scrapbook.httpStatusText[xhr.status];
          statusText = xhr.status + (statusText ? " " + statusText : "");
          reject(new Error(statusText));
        }
      };

      xhr.onabort = function (event) {
        // resolve with no param
        resolve();
      };

      xhr.onerror = function (event) {
        // No additional useful information can be get from the event object.
        reject(new Error("Network request failed."));
      };

      xhr.ontimeout = function (event) {
        reject(new Error("Request timeout."));
      };

      xhr.responseType = params.responseType;
      xhr.open(params.method || "GET", params.url, true);

      if (params.overrideMimeType) { xhr.overrideMimeType(params.overrideMimeType); }
      if (params.timeout) { xhr.timeout = params.timeout; }

      xhr.send();
    });
  },

  async sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  },

  async forEachAsync(iterable, cb, threshold = 100) {
    let t = Date.now();
    let i = 0;
    for (const item of iterable) {
      if (await cb(item, i++, iterable) === false) {
        break;
      }
      if (Date.now() - t > threshold) {
        await utils.sleep(0);
        t = Date.now();
      }
    }
  },
};

return utils;

}));
